import { generateBezier } from './bezier';
import Scrollbar from './scrollbar';
import { Color, Rect } from './common';
import theme from './theme'


const DIR_X = 1;
const DIR_Y = 2;

const SORT_NONE = 1; // 未排序
const SORT_ASC = 2; // 升序
const SORT_DESC = 3; // 降序


const TABLE_HEAD_FIXED = 1;
const TABLE_HEAD = 2;
const TABLE_BODY_FIXED = 3;
const TABLE_BODY = 4;

const IN_SCROLL_V = 1;
const IN_SCROLL_H = 2;
const IN_BAR_V = 3;
const IN_BAR_H = 4;

const Bezier = generateBezier(.14,.77,0,1);

function getNextSortType(type) {
    switch(type) {
        case SORT_NONE:
            return SORT_ASC;
        case SORT_ASC:
            return SORT_DESC;
        case SORT_DESC:
            return SORT_NONE;
        default:
            return SORT_NONE;
    }
}

function getOrDefault(v, defaultV) {
    return v === undefined ? defaultV : v;
}
function isFunction(f) {
    return typeof f === 'function';
}
function isObject(o) {
    return o !== null && typeof o === 'object';
}
function makeDelta(cur, last) {
    if (!cur || !last) {
        return {x: 0, y: 0}
    }
    return {
        x: -(cur.clientX - last.clientX),
        y: -(cur.clientY - last.clientY),
    }
}

function attenuationCoefficient(init_v, ms_from_begin) {
    const MAX_V = 100;
    if (init_v > MAX_V) {
        init_v = MAX_V;
    }
    const d = ms_from_begin;

    var percent = d / 5000 / (init_v/MAX_V);
    if (percent >= 1) {
        return 0;
    }
    const v = Bezier(percent, 1, 0);
    return v;
}
function valueBetween(v, min, max) {
    if (v < min) return min;
    if (v > max) return max;
    return v;
}
function triangle(ctx, p1, p2, p3, r) {
    let startX = (p1.x + p2.x) / 2, startY = (p1.y + p2.y) / 2;
    ctx.beginPath();
    ctx.moveTo(startX, startY);
    ctx.arcTo(p2.x, p2.y, p3.x, p3.y, r);
    ctx.arcTo(p3.x, p3.y, p1.x, p1.y, r);
    ctx.arcTo(p1.x, p1.y, p2.x, p2.y, r);
    ctx.closePath();
}
function drawSortIcon(ctx, x, y, w, h, sortType, style) {
    ctx.save();
    let color;
    if (sortType == SORT_NONE) {
        color = style.sortArrowColor;
    } else {
        color = style.sortArrowActiveColor;
    }
    ctx.fillStyle = color;
    ctx.strokeStyle = color;
    ctx.lineWidth = style.pixelRatio || 1;
    ctx.lineJoin = 'round';
    const r = (style.pixelRatio || 1) * 1;
    const arrowUp = [
        {x: x+w/2, y: y+h*0.1},
        {x: x+w*0.8, y: y+h*0.42},
        {x: x+w*0.2, y: y+h*0.42},
    ];
    ctx.beginPath();
    // let startX = (arrowUp[0].x + arrowUp[1].x) / 2, startY = (arrowUp[0].y + arrowUp[1].y) / 2;
    // ctx.moveTo(startX, startY);
    // ctx.arcTo(arrowUp[1].x, arrowUp[1].y, arrowUp[2].x, arrowUp[2].y, r);
    // ctx.arcTo(arrowUp[2].x, arrowUp[2].y, arrowUp[0].x, arrowUp[0].y, r);
    // ctx.arcTo(arrowUp[0].x, arrowUp[0].y, arrowUp[1].x, arrowUp[1].y, r);
    // ctx.closePath();
    triangle(ctx, arrowUp[0], arrowUp[1], arrowUp[2], r);
    ctx.stroke()
    if (sortType == SORT_ASC) {
        ctx.fill();
    }
    const arrowDown = [
        {x: x+w/2, y: y+h*0.9},
        {x: x+w*0.8, y: y+h*0.58},
        {x: x+w*0.2, y: y+h*0.58},
    ];
    ctx.beginPath();
    // startX = (arrowDown[0].x + arrowDown[1].x) / 2, startY = (arrowDown[0].y + arrowDown[1].y) / 2;
    // ctx.moveTo(startX, startY);
    // ctx.arcTo(arrowDown[1].x, arrowDown[1].y, arrowDown[2].x, arrowDown[2].y, r);
    // ctx.arcTo(arrowDown[2].x, arrowDown[2].y, arrowDown[0].x, arrowDown[0].y, r);
    // ctx.arcTo(arrowDown[0].x, arrowDown[0].y, arrowDown[1].x, arrowDown[1].y, r);
    // ctx.closePath();
    triangle(ctx, arrowDown[0], arrowDown[1], arrowDown[2], r);
    ctx.stroke()
    // 升序，亮起向下箭头
    if (sortType == SORT_DESC) {
        ctx.fill();
    }
    ctx.restore();
}
/**
 * 
 * @param {*} ctx 
 * @param {*} text 
 * @param {*} x 
 * @param {*} y 
 * @param {*} w 
 * @param {*} h 
 * @param {*} style => {color, fontSize, align, fontFamily}
 */
function drawTextInGrid(ctx, text, x, y, w, h, style) {
    const {fontSize, color, align, borderColor, borderWidth} = style;
    ctx.save();
    ctx.rect(x, y, w, h);
    ctx.clip();
    if (style.backgroundColor) {
        ctx.fillStyle = style.backgroundColor;
        ctx.fillRect(x, y, w, h);
    }
    ctx.font = `${fontSize}px monospace`;
    ctx.fillStyle = color || theme.COLOR;
    let xOffset = x;
    const yOffset = y + h - (h-fontSize) / 2;
    const textWidth = ctx.measureText(text).width;
    if (align === 'left') {
        xOffset = x;
    } else if (align === 'right') {
        xOffset = x + (w - textWidth);
    } else { // default: center
        xOffset = x + (w - textWidth)/2;
    }
    ctx.fillText(text, xOffset, yOffset);
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = borderWidth;
    ctx.beginPath();
    ctx.moveTo(x, y+h);
    ctx.lineTo(x+w, y+h);
    ctx.closePath();
    ctx.stroke();
    ctx.restore();
}
function drawTitleInGrid(ctx, text, x, y, w, h, style) {
    const {fontSize, color, align, borderColor, borderWidth, sort, sortType} = style;
    let sortIconWidth = 0, xOffsetIcon = 0;
    ctx.save();
    ctx.rect(x, y, w, h);
    ctx.clip();
    if (style.backgroundColor) {
        ctx.fillStyle = style.backgroundColor;
        ctx.fillRect(x, y, w, h);
    }
    ctx.font = `${fontSize}px monospace`;
    ctx.fillStyle = color || theme.COLOR;
    ctx.textAlign = 'left'; // 水平对齐设置
    ctx.textBaseline = 'middle'; // 垂直居中
    let xOffset = x;
    const yOffset = y + h / 2;
    if (sort) {
        sortIconWidth = fontSize;
    }
    const textWidth = ctx.measureText(text).width;
    const titleWidth = textWidth + sortIconWidth;
    if (align === 'left') {
        xOffset = x;
    } else if (align === 'right') {
        xOffset = x + (w - titleWidth);
    } else { // default: center
        xOffset = x + (w - titleWidth)/2;
    }
    xOffsetIcon = xOffset + textWidth;
    if (sort) {
        drawSortIcon(ctx, xOffsetIcon, y+((h-fontSize)/2), fontSize, fontSize, sortType, style);
    }

    ctx.fillText(text, xOffset, yOffset);
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = borderWidth;
    ctx.beginPath();
    ctx.moveTo(x, y+h);
    ctx.lineTo(x+w, y+h);
    ctx.closePath();
    ctx.stroke();
    ctx.restore();
}

function getCell(x, y, cols, rows, option) {
    let rowIndex = 0, colIndex = null, width = 0, row = null, col = null;
    rowIndex = Math.floor(y / option.rowHeight);
    for(let i=0; i<cols.length; i++) {
        width += cols[i].width_c;
        if (width >= x) {
            colIndex = i;
            break;
        }
    }
    if (rows && rows.length > 0 && rows[rowIndex] !== undefined) {
        row = rows[rowIndex];
    }
    if (colIndex !== null) {
        col = cols[colIndex];
    }
    return {row, col}
}

/*
    fixed          normal
 ------------------------------
 | head_fixed |    head        |
 |------------|----------------|
 |            |                |
 | body_fixed |    body        |
 |            |                |
 ------------------------------
 */
export class CanvasTable {
    constructor(rootElm) {
        // 不做太多初始化，仅创建canvas元素
        this.rootElm = rootElm;
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d');
        rootElm.appendChild(this.canvas);
        this.bindEvent();

        this.posX = 0;
        this.posY = 0; // 当前显示位置坐标 (CSS坐标)

        this.fixedWidth = 0; // 左侧固定总列宽
        this.touchDir = null;
        this.startPoint = null;
        this.lastPoint = null;
        this.requestFlag = false;
        this.touchMoveVelocity = 0;

        this.hasScrollbarV = false;
        this.hasScrollbarH = false;
        this.scrollbarH = null;
        this.scrollbarV = null;

        this.waitingRender = false;
        this.sortObject = {
            field: null,
            type: SORT_NONE,
        }
    }
    get top() {
        return this.posY * this.pixelRatio;
    }
    set top(value) {
        const y = value / this.pixelRatio;
        this.posY = valueBetween(y, 0, this.C_MAX_POS_Y);
        this.requestRefresh();
    }
    get left() {
        return this.posX * this.pixelRatio;
    }
    set left(value) {
        const x = value / this.pixelRatio;
        this.posX = valueBetween(x, 0, this.C_MAX_POS_X);
        this.requestRefresh();
    }
    get nextSortObject() {
        const sortObject = this.sortObject;
        if (!sortObject) return null;
        return {
            field: sortObject.field,
            type: getNextSortType(sortObject.type),
        }
    }
    // 这里有问题，需要改进，分两种转换，一种是 设计尺寸到canvas像素， 一种是css像素到canvas像素
    design_to_canvas_px(x) {
        if (x === undefined || x === null) return x;
        return x * this.designScale;
    }
    canvas_to_css_px(x) {
        if (x === undefined || x === null) return x;
        return x / this.pixelRatio;
    }
    css_to_canvas_px(x) {
        if (x === undefined || x === null) return x;
        return x * this.pixelRatio;
    }
    initOption(option) {
        const { designWidth, sortObject, top, left } = option;
        const { ctx } = this;
        // pixelRatio: 像素比例，canvas像素比css像素的值，就是1个css像素对应几个canvas像素
        // designScale: canvas宽度与设计稿宽度的比例，不指定设计稿宽度则为1，此值用来计算实际绘制尺寸，比如线宽、文字大小等。 drawValue = valueSpecified * designScale;
        this.pixelRatio = window.devicePixelRatio; 
        const boundingRect = this.rootElm.getBoundingClientRect();
        this.cssWidth = boundingRect.width;

        this.canvasWidth = boundingRect.width * this.pixelRatio;
        this.designWidth = designWidth || boundingRect.width * this.pixelRatio;
        if (this.canvasWidth < this.designWidth) {
            this.canvasWidth = this.designWidth;
            this.pixelRatio = this.canvasWidth / boundingRect.width;
        }
        
        this.designScale = this.canvasWidth / this.designWidth; // 在不指定designWidth的情况下，为1，值域大于等于1

        this.fillDefaultOption(option);

        let cssHeight = Math.max(boundingRect.height, option.maxHeight);
        const tableCssHeight = Math.ceil((option.list.length * option.rowHeight + option.headHeight + option.scrollbarWidth) / this.pixelRatio);
        cssHeight = Math.min(cssHeight, tableCssHeight);
        this.canvasHeight = cssHeight * this.pixelRatio;

        this.cssHeight = cssHeight;
        this.canvas.style.display = 'block';
        this.canvas.style.width = '100%';
        this.canvas.style.height = cssHeight + 'px';

        // viewWidth, viewHeight 主canvas可视区的宽高，排除滚动条的
        this.canvas.width = this.canvasWidth;
        this.canvas.height = this.canvasHeight;

        // headHeight 和 fixedWidth 不受滚动条影响。
        // 受滚动条影响的是 viewWidth 和 viewHeight
        const { headHeight } = option;
        const fixedWidth = option.fixedCols.reduce((acc, i) => {
            return acc + getOrDefault(i.width_c, option.colWidth);
        }, 0);
        const tableContentWidth = option.normalCols.reduce((acc, i) => {
            return acc + getOrDefault(i.width_c, option.colWidth);
        }, 0);
        const tableContentHeight = option.list.length * option.rowHeight;

        const tableWidth = fixedWidth + tableContentWidth;
        const tableHeight = headHeight + tableContentHeight;
        this.tableContentWidth = tableContentWidth;

        // 默认没有滚动条，
        this.viewWidth = this.canvasWidth;
        this.viewHeight = this.canvasHeight;
        this.hasScrollbarV = false;
        this.hasScrollbarH = false;

        // 计算scrollbar， 修正viewWidth 和 viewHeight
        if (!this.hasScrollbarV && tableHeight > this.viewHeight) {
            // 内容高度超出可显示区域，需要垂直滚动条
            this.hasScrollbarV = true;
            this.viewWidth = this.canvasWidth - option.scrollbarWidth;
        }
        if (!this.hasScrollbarH && tableWidth > this.viewWidth) {
            // 内容宽度超出可显示区域，需要水平滚动条
            this.hasScrollbarH = true;
            this.viewHeight = this.canvasHeight - option.scrollbarWidth;
        }
        // 增加水平滚动条会导致viewHeight变小，可能出现垂直滚动条
        if (!this.hasScrollbarV && tableHeight > this.viewHeight) {
            // 内容高度超出可显示区域，需要垂直滚动条            
            this.hasScrollbarV = true;
            this.viewWidth = this.canvasHeight - option.scrollbarWidth;
        }

        this.tableWidth = tableWidth;
        this.tableHeight = tableHeight;
        this.fixedWidth = fixedWidth;
        this.headHeight = headHeight;

        if (this.hasScrollbarV) {
            this.scrollbarV = new Scrollbar({
                ctx,
                type: Scrollbar.VER,
                scrollHeight: tableContentHeight,
                clientHeight: this.viewHeight - this.headHeight,
                rect: new Rect(this.viewWidth, this.headHeight, option.scrollbarWidth, this.viewHeight - this.headHeight),
                style: {
                    foregroundColor: option.scrollbarForegroundColor,
                    backgroundColor: option.scrollbarBackgroundColor,
                }
            })
        }
        if (this.hasScrollbarH) {
            this.scrollbarH = new Scrollbar({
                ctx,
                type: Scrollbar.HOR,
                scrollWidth: tableContentWidth,
                clientWidth: this.viewWidth - this.fixedWidth,
                rect: new Rect(0, this.viewHeight, this.viewWidth, option.scrollbarWidth),
                style: {
                    foregroundColor: option.scrollbarForegroundColor,
                    backgroundColor: option.scrollbarBackgroundColor,
                }
            })
        }


        this.C_MAX_POS_X = this.canvas_to_css_px(Math.max(tableWidth - this.viewWidth, 0));
        this.C_MAX_POS_Y = this.canvas_to_css_px(Math.max(tableHeight - this.viewHeight, 0));

        this.waitingRender = false;
        this.top = top || 0;
        this.left = left || 0;
        if (sortObject) {
            this.sortObject =  sortObject;
        }
    }
    fillDefaultOption(option) {
        const initValue = (v, dv) => {
            if (v === undefined || v === null) {
                return this.design_to_canvas_px(dv);
            } else {
                return this.design_to_canvas_px(v);
            }
        }
        option.color = getOrDefault(option.color, theme.COLOR);
        option.backgroundColor = getOrDefault(option.backgroundColor, theme.BACKGROUND_COLOR);
        option.borderColor = getOrDefault(option.borderColor, theme.BORDER_COLOR);
        option.borderWidth = initValue(option.borderWidth, 1);
        option.headColor = getOrDefault(option.headColor, option.color);
        option.headBackgroundColor = getOrDefault(option.headBackgroundColor, option.backgroundColor);
        option.rowHeight = initValue(option.rowHeight, 100);
        option.headHeight = getOrDefault(this.design_to_canvas_px(option.headHeight), option.rowHeight);
        option.colWidth = initValue(option.colWidth, 200);
        option.fontSize = initValue(option.fontSize, 28);
        option.scrollbarWidth = initValue(option.scrollbarWidth, 16);
        option.maxHeight = getOrDefault(option.maxHeight, 0); // css value

        option.sortArrowColor = getOrDefault(option.sortArrowColor, theme.ARROW_COLOR);
        option.sortArrowActiveColor = getOrDefault(option.sortArrowActiveColor, theme.ARROW_ACTIVE_COLOR);
        option.moreArrowColor = getOrDefault(option.moreArrowColor, theme.MORE_ARROW_COLOR);
        option.moreArrowSize = initValue(option.moreArrowSize, 32);

        option.fixedCols = [];
        option.normalCols = [];
        option.cols.forEach(col => {
            col.align = getOrDefault(col.align, 'center');
            col.titleAlign = getOrDefault(col.titleAlign, col.align);
            col.sort = getOrDefault(col.sort, false);
            col.fixed = getOrDefault(col.fixed, false);
            col.width_c = this.design_to_canvas_px(col.width) || option.colWidth; // 每列的canvas像素宽度

            if (col.fixed) {
                option.fixedCols.push(col);
            } else {
                option.normalCols.push(col);
            }
        })
    }
    bindEvent() {
        this.canvas.addEventListener('wheel', e => {
            this.stopInertiaScroll();
            var deltaY = Math.round(e.deltaY);
            var deltaX = Math.round(e.deltaX);
            var ok1 = this.addPosY(deltaY);
            var ok2 = this.addPosX(deltaX);
            if (ok1 && ok2) {
                e.preventDefault();
                e.stopPropagation();
            }
        });
        this.touchHandler = this.touchHandler.bind(this);
        this.canvas.addEventListener('touchstart', this.touchHandler);
        this.canvas.addEventListener('touchmove', this.touchHandler);
        this.canvas.addEventListener('touchend', this.touchHandler);
        this.canvas.addEventListener('touchcancel', this.touchHandler);

        this.canvas.addEventListener('contextmenu', e => {
            e.preventDefault();
            e.stopPropagation();
        });
        this.mouseHandler = this.mouseHandler.bind(this);
        this.canvas.addEventListener('mousedown', this.mouseHandler);
        window.addEventListener('mousemove', this.mouseHandler);
        window.addEventListener('mouseup', this.mouseHandler);

        this.canvas.addEventListener('click', this.clickHandler.bind(this));
    }
    removeEventHandler() {
        window.removeEventListener('mousemove', this.mouseHandler);
        window.removeEventListener('mouseup', this.mouseHandler);
    }
    clickHandler(e) {
        const x = e.clientX, y = e.clientY;
        if (this.mousedownPoint && this.mousedownPoint.clientX == x &&
        this.mousedownPoint.clientY == y) {
            var rect = this.canvas.getBoundingClientRect()
            const cx = Math.floor(x - rect.left);
            const cy = Math.floor(y - rect.top);
            const res = this.testInScrollbar(cx, cy);
            if (res > 0) return; // 滚动条区域，忽略
            var vx = this.css_to_canvas_px(cx);
            var vy = this.css_to_canvas_px(cy);
            var rx = vx + this.left;
            var ry = vy + this.top;

            const {option} = this;
            const {normalCols, fixedCols, list} = option;
            let cell = null, area = null;

            // 四个区域
            if (vx >= this.fixedWidth && vy >= this.headHeight) {
                rx -= this.fixedWidth;
                ry -= this.headHeight;
                area = TABLE_BODY;
                cell = getCell(rx, ry, normalCols, list, option);
            } else if (vx >= this.fixedWidth) {
                rx -= this.fixedWidth;
                area = TABLE_HEAD;
                cell = getCell(rx, ry, normalCols, [], option);
            } else if (vy >= this.headHeight) {
                ry -= this.headHeight;
                area = TABLE_BODY;
                cell = getCell(rx, ry, fixedCols, list, option)
            } else {
                area = TABLE_HEAD;
                cell = getCell(rx, ry, fixedCols, [], option);
            }
            if (cell.col) {
                this.clickCell({...cell, area});
            }
            if (area === TABLE_HEAD && cell.col) {
                const col = cell.col;
                let sortType = SORT_ASC;
                if (col.field == this.sortObject.field) {
                    sortType = getNextSortType(this.sortObject.type);
                }
                if (col.sort) {
                    if (isFunction(this.onsort)) {
                        this.onsort(cell, {
                            field: col.field,
                            type: sortType
                        });
                    }
                }
            }
        }
    }
    clickCell(cell) {
        if (isFunction(this.onclick)) {
            this.onclick(cell);
        }
    }
    mouseHandler(e) {
        var rect = this.canvas.getBoundingClientRect()
        var res
        const cx = Math.floor(e.clientX - rect.left);
        const cy = Math.floor(e.clientY - rect.top);

        switch(e.type) {
            case 'mousedown':
                this.mousedownPoint = e;
                this.stopInertiaScroll();
                res = this.testInScrollbar(cx, cy);
                if (res > 0) {
                    this.mouseScrolling = true;
                    if (res == IN_SCROLL_V) {
                        this.setScrollbarPosition(cy, DIR_Y);
                    } else if (res == IN_SCROLL_H) {
                        this.setScrollbarPosition(cx, DIR_X);
                    }
                    this.firstDraggingPoint = e;
                    this.lastDraggingPoint = e;
                    this.lastDraggingArea = res;
                    this.draggingStartTop = this.top;
                    this.draggingStartLeft = this.left;
                }
                break;
            case 'mousemove':
                if (this.mouseScrolling) {
                    const d = makeDelta(e, this.firstDraggingPoint);
                    if (this.lastDraggingArea == IN_SCROLL_V || this.lastDraggingArea == IN_BAR_V) {
                        const dy = d.y;
                        const scale = this.scrollbarV.verticalPixelRatio;
                        this.top = this.draggingStartTop - (scale * dy * this.pixelRatio);
                    } else {
                        const dx = d.x;
                        const scale = this.scrollbarH.horizontalPixelRatio;
                        this.left = this.draggingStartLeft - (scale * (dx * this.pixelRatio));
                    }
                    this.lastDraggingPoint = e;
                }
                break;
            case 'mouseup':
                this.mouseScrolling = false;
                break;
        }
    }
    touchHandler(e) {
        // console.log(e.type);
        // clientXY - 当前视口
        // pageXY - 当前页面
        // screen - 显示屏幕坐标
        let point, delta;
        var prevent_default = true;
        var rect, cx, cy, res;
        switch(e.type) {
            case 'touchstart':
                this.stopInertiaScroll();
                point = e.touches[0];

                // 在滚动条区域，不处理
                rect = this.canvas.getBoundingClientRect()
                cx = Math.floor(point.clientX - rect.left);
                cy = Math.floor(point.clientY - rect.top);
                res = this.testInScrollbar(cx, cy);
                if (res > 0) {
                    return;
                }

                this.lastTouchEventTimpStamp = Date.now();
                this.touchDir = null;
                this.startPoint = point;
                this.lastPoint = point;
                this.touchMoveVelocity = 0;
                this.touching = true;
                break;
            case 'touchmove':
                if (!this.touching) return;
                point = e.touches[0];
                delta = makeDelta(point, this.lastPoint);
                if (!this.touchDir) {
                    const dx = Math.abs(delta.x), dy = Math.abs(delta.y);
                    if (dx > dy && dx > 4) {
                        this.touchDir = DIR_X;
                    } else if (dy > dx && dy > 4) {
                        this.touchDir = DIR_Y;
                    }
                }
                if (this.touchDir) {
                    this.lastPoint = point;
                }
                if (this.touchDir === DIR_Y) {
                    prevent_default = this.addPosY(delta.y);
                } else if (this.touchDir === DIR_X) {
                    prevent_default = this.addPosX(delta.x);
                }
                // 在这里求速度，根据point 和 lastPoint所发生的时间和像素距离  x px / 100 ms
                if (this.touchDir) {
                    const d = (this.touchDir === DIR_X ? delta.x : delta.y) * 100 / (Date.now() - this.lastTouchEventTimpStamp);
                    this.touchMoveVelocity = Math.floor((this.touchMoveVelocity * 0.3) +  (d * 0.7));
                }
                break;
            case 'touchend':
                point = e.touches[0];
                delta = makeDelta(point, this.lastPoint);
                if (this.touchDir === DIR_Y) {
                    this.addPosY(delta.y);
                } else if (this.touchDir === DIR_X) {
                    this.addPosX(delta.x);
                }
                if (isFinite(this.touchMoveVelocity)) {
                    this.startInertiaScroll(this.touchMoveVelocity, this.touchDir);
                }
                this.touchDir = null;
                this.touching = false;
                this.lastPoint = null;
                this.touchMoveVelocity = null;
                break;
            case 'touchcancel':
                this.touching = false;
                this.touchDir = null;
                break;
        }
        // 如果不阻止默认行为，则滑动列表时，整个页面也滑动。
        if (e.type == 'touchmove' && prevent_default) {
            e.preventDefault();
            e.stopPropagation();
        }
    }
    testInScrollbar(cx, cy) {
        if (!this.hasScrollbarV && !this.hasScrollbarH) {
            return 0;
        }
        const x = cx * this.pixelRatio, y = cy * this.pixelRatio;
        if (this.scrollbarV) {
            const res = this.scrollbarV.testIn(x, y)
            if (res == 1) {
                return IN_SCROLL_V; // 垂直滚动条区域
            } else if (res == 2) {
                return IN_BAR_V; // 垂直滚动条滑块
            }
        }
        if (this.scrollbarH) {
            const res = this.scrollbarH.testIn(x, y);
            if (res == 1) {
                return IN_SCROLL_H;
            } else if (res == 2) {
                return IN_BAR_H;
            }
        }
        return 0;
    }
    // 带惯性的滚动
    inertiaScroll(v, dir) { // v = px / 5ms
        window.requestAnimationFrame((high_t) => { // t ms
            const t = Math.floor(high_t);
            if (!this.inertiaBeginTimeStamp) {
                this.inertiaBeginTimeStamp = t;
            }
            if (!this.lastInertiaTimeStamp) {
                this.lastInertiaTimeStamp = t;
            }
            const delta_t = (t - this.lastInertiaTimeStamp) / 100; // 转为100ms单位
            const c = attenuationCoefficient(Math.abs(v), t - this.inertiaBeginTimeStamp);
            const s = Math.floor(delta_t * v * c * 8); // 乘以一个以时间和初始速度相关的衰减值, *5
            if (Math.abs(s) >= 1) {
                this.lastInertiaTimeStamp = t;
                const ok = dir === DIR_X ? this.addPosX(s) : this.addPosY(s);
                if (!ok) {
                    this.inertia = false;
                }
            } else if (delta_t != 0) {
                this.inertia = false;
            }
            if (this.inertia) {
                this.inertiaScroll(v, dir);
            } else {
                this.inertiaBeginTimeStamp = null;
                this.lastInertiaTimeStamp = null;
                this.touchDir = null;
            }
        })
    }
    startInertiaScroll(delta, dir) {
        this.inertia = true;
        this.inertiaScroll(delta, dir)
    }
    stopInertiaScroll() {
        this.inertia = false;
    }
    // dir = DIR_X / DIR_Y
    setScrollbarPosition(px, dir) {
        var d = px * this.pixelRatio;
        var v, offsetPx;
        if (dir === DIR_Y) {
            if (this.scrollbarV) {
                const scrollTop = this.scrollbarV.setPosition(0, d);
                offsetPx = Math.floor(scrollTop / this.pixelRatio);
                this.addPosY(offsetPx - this.posY);
            }
        } else if (dir === DIR_X) {
            if (this.scrollbarH) {
                const scrollLeft = this.scrollbarH.setPosition(d, 0);
                offsetPx = Math.floor(scrollLeft / this.pixelRatio);
                this.addPosX(offsetPx - this.posX);
            }
        }
    }
    addPosX(delta) {
        let ok = true;
        this.posX += delta;
        if (this.posX < 0) {
            this.posX = 0;
            ok = false;
        }
        if (this.posX > this.C_MAX_POS_X) {
            this.posX = this.C_MAX_POS_X;
            ok = false;
        }
        this.requestRefresh();
        return ok;
    }
    addPosY(delta) {
        let ok = true;
        this.posY += delta;
        if (this.posY < 0) {
            this.posY = 0;
            ok = false;
        }
        if (this.posY > this.C_MAX_POS_Y) {
            this.posY = this.C_MAX_POS_Y;
            ok = false;
        }
        this.requestRefresh();
        return ok;
    }
    renderScrollbar() {
        const barWidth = this.option.scrollbarWidth;
        const ctx = this.ctx;

        // 垂直滚动条
        if (this.hasScrollbarV) {
            this.scrollbarV.scrollTop = this.top;
        }
        // 水平滚动条
        if (this.hasScrollbarH) {
            this.scrollbarH.scrollLeft = this.left;
        }
        if (this.hasScrollbarV && this.hasScrollbarH) {
            // 画交界区域
            ctx.fillStyle = this.option.scrollbarCornerColor || theme.SCROLL_CORNER_COLOR;
            ctx.fillRect(this.viewWidth, this.viewHeight, barWidth, barWidth);
        }
    }
    translateContext(ctx, id) {
        const {top, left, fixedWidth, headHeight} = this;
        ctx.moveTo(0,0);
        switch(id) {
            case TABLE_BODY_FIXED:
                ctx.translate(0, -top + headHeight);
                return;
            case TABLE_BODY:
                ctx.translate(-left + fixedWidth, -top + headHeight);
                return;
            case TABLE_HEAD_FIXED:
                ctx.translate(0, 0);
                return;
            case TABLE_HEAD:
                ctx.translate(-left + fixedWidth, 0);
                return;
            default:
                return {};
        }
    }
    // render content
    renderBody(rect, cols, canvasId) {
        const {list, colWidth, color, rowHeight, borderColor, borderWidth} = this.option;
        const { ctx } = this;
        let x = 0, y = 0;
        ctx.save();
        ctx.rect(rect.x, rect.y, rect.w, rect.h);
        ctx.clip();
        this.translateContext(ctx, canvasId);
        const bodyTop = this.top;
        const bodyBottom = this.top + this.viewHeight - this.headHeight;
        list.forEach(row => {
            x = 0;
            if (y + rowHeight < bodyTop || y > bodyBottom) {
                y += rowHeight;
                return;
            }
            cols.forEach(col => {
                var fontSize = this.option.fontSize;
                let textColor = color;
                const width = col.width_c;
                let text;
                if (isFunction(col.draw)) {
                    const ok = col.draw(row, ctx, new Rect(x, y, width, rowHeight), this.designScale);
                    if (!ok) { this.waitingRender = true; }
                } else {
                    if (isFunction(col.formatter)) {
                        text = col.formatter(row);
                    } else {
                        text = row[col.field]
                    }
                    if (isObject(text)) {
                        const o = text;
                        text = o.text;
                        textColor = o.color || textColor;
                        fontSize = o.fontSize || fontSize;
                    }
                    drawTextInGrid(ctx, text, x, y, width, rowHeight, {
                        fontSize,
                        color: textColor,
                        align: col.align,
                        borderColor,
                        borderWidth,
                        backgroundColor: row.backgroundColor,
                    });
                }
                x += width;
            });
            y += rowHeight;
        });
        ctx.restore();
    }
    // render head
    renderHead(rect, cols, canvasId) {
        const {colWidth, headColor, headHeight, borderColor, borderWidth, headBackgroundColor} = this.option;
        const { ctx } = this;
        let x = 0, y = 0;
        ctx.save();
        ctx.rect(rect.x, rect.y, rect.w, rect.h);
        ctx.clip();
        ctx.fillStyle = headBackgroundColor;
        ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
        this.translateContext(ctx, canvasId);
        cols.forEach(col => {
            var fontSize = this.option.fontSize;
            let textColor = headColor;
            const width = col.width_c;
            let text = col.title;
            if (isObject(text)) {
                const o = text;
                text = o.text;
                textColor = o.color || textColor;
                fontSize = o.fontSize || fontSize;
            }
            const sortType = this.sortObject.field == col.field ? this.sortObject.type : SORT_NONE;
            drawTitleInGrid(ctx, text, x, y, width, headHeight, {
                ...this.option,
                fontSize,
                color: textColor,
                align: col.titleAlign || col.align,
                borderColor,
                borderWidth,
                backgroundColor: headBackgroundColor,
                sort: col.sort,
                sortType,
                pixelRatio: this.pixelRatio,
            });
            x += width;
        });
        ctx.restore();
    }
    renderSubView() {
        let rect;
        const {normalCols, fixedCols} = this.option;
        const { headHeight, fixedWidth, viewWidth, viewHeight } = this;
        rect = {x: 0, y: 0, w: fixedWidth, h: headHeight};
        this.renderHead(rect, fixedCols, TABLE_HEAD_FIXED);
        // rect = {x: fixedWidth, y: 0, w: viewWidth - fixedWidth, h: headHeight}
        rect = {x: fixedWidth, y: 0, w: this.canvasWidth - fixedWidth, h: headHeight}
        this.renderHead(rect, normalCols, TABLE_HEAD);
        rect = {x: 0, y: headHeight, w: fixedWidth, h: viewHeight - headHeight}
        this.renderBody(rect, fixedCols, TABLE_BODY_FIXED);
        rect = {x: fixedWidth, y: headHeight, w: viewWidth - fixedWidth, h: viewHeight - headHeight}
        this.renderBody(rect, normalCols, TABLE_BODY);
    }
    renderArrow() {
        const ctx = this.ctx;
        const size = this.option.moreArrowSize;
        if (!this.hasScrollbarH) {
            return;
        }
        if (this.tableContentWidth - size <= this.left + this.viewWidth - this.fixedWidth) {
            return;
        }
        const rectSize = this.option.headHeight;
        let top = 0, left = this.canvasWidth - rectSize, width = rectSize, height = rectSize;
        ctx.save();
        ctx.rect(left, top, width, height);
        const bg = this.option.headBackgroundColor;
        if (!bg || bg === 'transparent') {
            ctx.fillStyle = 'transparent';
        } else if (typeof bg === 'string') {
            const gradient = ctx.createLinearGradient(left, top, left+width, top);
            const color = new Color(bg);
            color.a = 0;
            gradient.addColorStop(0, color.rgba);
            gradient.addColorStop(.5, bg);
            gradient.addColorStop(1, bg);
            ctx.fillStyle = gradient;
        }
        ctx.fill();
        // draw triangle
        top = top + (height - size) / 2;
        left = this.canvasWidth - 4 * this.designScale - size;
        width = size;
        height = size;
        const p1 = {x: left + width * 0.2, y: top + height * 0.1};
        const p2 = {x: left + width - width * 0.2, y: top + height * 0.5};
        const p3 = {x: left + width * 0.2, y: top + height - height * 0.1};
        triangle(ctx, p1, p2, p3, 3 * this.designScale);
        ctx.fillStyle = this.option.moreArrowColor;
        ctx.fill();
        ctx.restore();
    }
    init(option) {
        if (!option || typeof option !== 'object') {
            throw `CanvasTable: option Invalid: ${option}`;
        }
        if (!(option.cols instanceof Array)) {
            throw `CanvasTable: option.cols Invalid: ${option.cols}`;
        }
        if (!(option.list instanceof Array)) {
            throw `CanvasTable: option.list Invalid: ${option.list}`;
        }

        this.option = option;
        
        this.initOption(option);
        
        window.requestAnimationFrame(this.draw.bind(this));
    }
    requestRefresh() {
        if (!this.requestFlag) {
            this.requestFlag = true;
            window.requestAnimationFrame(this.draw.bind(this));
        }
    }
    draw() {
        const { fixedWidth, headHeight } = this;

        const ctx = this.ctx;
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        ctx.save();
        
        this.renderSubView();
        this.renderScrollbar();
        this.renderArrow();

        ctx.restore();

        this.requestFlag = false;

        if (this.waitingRender) {
            this.requestRefresh();
            this.waitingRender = false;
        }
    }
    exportHtml(titleText) {
        const {list, cols} = this.option;
        const html = document.createElement('html');
        const head = document.createElement('head');
        const meta = document.createElement('meta');
        meta.setAttribute('charset', 'utf-8');
        const title = document.createElement('title');
        const style = document.createElement('style');
        style.innerText = '\
table {\
    border-collapse: collapse;\
}\
th, td {\
    border: 1px solid #999;\
    text-align: center;\
    padding: 0 4px;\
}'
        title.innerHTML = titleText;
        head.appendChild(meta);
        head.appendChild(title);
        head.appendChild(style);
        const body = document.createElement('body');
        html.appendChild(head);
        html.appendChild(body);
        const table = document.createElement('table');
        const thead = document.createElement('thead');
        const tbody = document.createElement('tbody');
        table.appendChild(thead);
        table.appendChild(tbody);
        const head_tr = document.createElement('tr');
        cols.forEach(col => {
            const th = document.createElement('th');
            let text = '';
            if (isObject(col.title)) {
                text = col.title.text;
            } else {
                text = col.title;
            }
            th.innerText = text;
            head_tr.appendChild(th);
        })
        thead.appendChild(head_tr);
        list.forEach(row => {
            const tr = document.createElement('tr');
            cols.forEach(col => {
                const td = document.createElement('td');
                let text = '';
                if (isFunction(col.formatter)) {
                    text = col.formatter(row);
                } else {
                    text = row[col.field]
                }
                if (isObject(text)) {
                    const o = text;
                    text = o.text;
                }
                td.innerText = text;
                tr.appendChild(td);
            })
            tbody.appendChild(tr);
        })
        body.appendChild(table);
        return '<!DOCTYPE html>' + html.outerHTML;
    }
    dispose() {
        if (this.rootElm) {
            this.rootElm.innerHTML = '';
            this.rootElm = null;
        }
        this.removeEventHandler();
    }
}

CanvasTable.SORT_NONE = SORT_NONE;
CanvasTable.SORT_ASC = SORT_ASC;
CanvasTable.SORT_DESC = SORT_DESC;
